﻿using System;
using System.Globalization;
using System.Text;

using Microsoft.SqlServer.Dts.Pipeline;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;

namespace DatabaseGroup.SSIS.SourceAdapters
{
    /// <summary>
    /// Interface to describe the component buffer data functions provided
    /// </summary>
    internal interface IComponentBufferService
    {
        /// <summary>
        /// Adds a row to the error output
        /// </summary>
        /// <param name="errorCode">The error code for the row</param>
        /// <param name="errorMessage">A message describing the error</param>
        /// <param name="lineNumber">The source file line number the error occurred on</param>
        /// <param name="rowData">The row data that caused the error</param>
        /// <param name="isInvalid">True if the error was fatal to standard output processing</param>
        /// <param name="columnName">The column name the error occurred on</param>
        /// <param name="columnData">The column data that caused the error</param>
        void AddErrorRow(int errorCode, string errorMessage, long lineNumber, string rowData, bool isInvalid, string columnName, string columnData);
        
        /// <summary>
        /// Adds a row to the log output
        /// </summary>
        /// <param name="errorCode">The error code for the row</param>
        /// <param name="errorMessage">A message describing the error</param>
        /// <param name="lineNumber">The source file line number the error occurred on</param>
        /// <param name="columnName">The column name the error occurred on</param>
        /// <param name="columnData">The column data that caused the error</param>
        void AddLogRow(int errorCode, string errorMessage, long lineNumber, string columnName, string columnData);
        
        /// <summary>
        /// Adds a new row to the standard output buffer
        /// </summary>
        void AddRow();

        /// <summary>
        /// Gets the number of columns expected for use
        /// </summary>
        int ColumnCount { get; }

        /// <summary>
        /// Gets if the error output is being used
        /// </summary>
        bool ErrorOutputUsed { get; }

        /// <summary>
        /// Gets if the log output is being used
        /// </summary>
        bool LogOutputUsed { get; }

        /// <summary>
        /// Gets or sets the log output auditing configuration
        /// </summary>
        AuditingConfiguration LogAuditingConfig { get; set; }

        /// <summary>
        /// Gets or sets the error output auditing configuration
        /// </summary>
        AuditingConfiguration ErrorAuditingConfig { get; set; }
       
        /// <summary>
        /// Removes the current row from the standard output buffer
        /// </summary>
        void RemoveRow();

        /// <summary>
        /// Assigns data to a specific column on the standard output buffer
        /// for the current row - converting the data to the correct type first
        /// </summary>
        /// <param name="index">The zero-based column index</param>
        /// <param name="columnData">The data to assign to the column</param>
        void SetColumnData(int index, string columnData);

        /// <summary>
        /// Assigns a null value to a specific column on the standard output 
        /// buffer for the current row
        /// </summary>
        /// <param name="index">The zero-based column index</param>
        void SetNull(int index);
    }

    /// <summary>
    /// Class to provide component buffer data functions
    /// </summary>
    internal class ComponentBufferService : IComponentBufferService
    {
        /// <summary>
        /// Gets or sets the code page encoding to use for string data
        /// </summary>
        internal Encoding OutputEncoding { get; set; }

        /// <summary>
        /// Gets or sets the locale to use for data formats (i.e. datetimes)
        /// </summary>
        internal int Locale { get; set; }

        private readonly OutputRuntimePropertiesCache _mainOutputProps;
        private readonly OutputRuntimePropertiesCache _logOutputProps;
        private readonly OutputRuntimePropertiesCache _errorOutputProps;
        private readonly ASCIIEncoding _asciiEncoding = new ASCIIEncoding();
        private readonly UnicodeEncoding _unicodeEncoding = new UnicodeEncoding();
        
        /// <summary>
        /// Initializes a new instance of the ComponentBufferService class 
        /// using caches containing the outputs configuration
        /// </summary>
        /// <param name="mainOutputProps">A cache of properties for the main/standard output</param>
        /// <param name="logOutputProps">A cache of properties for the log output</param>
        /// <param name="errorOutputProps">A cache of properties for the error output</param>
        public ComponentBufferService(OutputRuntimePropertiesCache mainOutputProps, OutputRuntimePropertiesCache logOutputProps, OutputRuntimePropertiesCache errorOutputProps)
        {
            ArgumentVerifier.CheckObjectArgument(mainOutputProps, "mainOutputProps");

            _mainOutputProps = mainOutputProps;
            _logOutputProps = logOutputProps;
            _errorOutputProps = errorOutputProps;

            this.OutputEncoding = Encoding.GetEncoding(PropertiesManager.DefaultCodePage);
            this.Locale = PropertiesManager.DefaultLocale;
        }

        /// <summary>
        /// Gets if the error output is being used
        /// </summary>
        public bool ErrorOutputUsed
        {
            get { return null != _errorOutputProps.Buffer; }
        }

        /// <summary>
        /// Gets if the log output is being used
        /// </summary>
        public bool LogOutputUsed
        {
            get { return null != _logOutputProps.Buffer; }
        }

        /// <summary>
        /// Gets or sets the log output auditing configuration
        /// </summary>
        public AuditingConfiguration LogAuditingConfig { get; set; }

        /// <summary>
        /// Gets or sets the error output auditing configuration
        /// </summary>
        public AuditingConfiguration ErrorAuditingConfig { get; set; }

        /// <summary>
        /// Gets the number of columns expected for use
        /// </summary>
        public int ColumnCount
        {
            get { return _mainOutputProps.Count; }
        }

        /// <summary>
        /// Adds a new row to the standard output buffer
        /// </summary>
        public void AddRow()
        {
            _mainOutputProps.Buffer.AddRow();
        }

        /// <summary>
        /// Removes the current row from the standard output buffer
        /// </summary>
        public void RemoveRow()
        {
            _mainOutputProps.Buffer.RemoveRow();
        }

        /// <summary>
        /// Assigns data to a specific column on the standard output buffer
        /// for the current row - converting the data to the correct type first
        /// </summary>
        /// <param name="index">The zero-based column index</param>
        /// <param name="columnData">The data to assign to the column</param>
        public void SetColumnData(int index, string columnData)
        {
            // the data is assigned to the column different ways depending on
            // the columns datatype
            switch (_mainOutputProps[index].DataType)
            {
                case DataType.DT_BOOL:
                    switch ((columnData ?? "").ToUpper())
                    {
                        case "1":
                        case "TRUE":
                            _mainOutputProps.Buffer.SetBoolean(_mainOutputProps[index].BufferColumnIndex, true);
                            break;
                        case "0":
                        case "FALSE":
                            _mainOutputProps.Buffer.SetBoolean(_mainOutputProps[index].BufferColumnIndex, false);
                            break;
                        default:
                            _mainOutputProps.Buffer.SetBoolean(_mainOutputProps[index].BufferColumnIndex, bool.Parse(columnData));
                            break;
                    }
                    break;
                case DataType.DT_BYTES:
                    _mainOutputProps.Buffer.SetBytes(_mainOutputProps[index].BufferColumnIndex, this.OutputEncoding.GetBytes(columnData));
                    break;
                case DataType.DT_CY:
                case DataType.DT_DECIMAL:
                case DataType.DT_NUMERIC:
                    // TODO: consider linking number style to Locale
                    _mainOutputProps.Buffer.SetDecimal(_mainOutputProps[index].BufferColumnIndex, decimal.Parse(columnData));
                    break;
                case DataType.DT_DATE:
                case DataType.DT_DBTIMESTAMP:
                case DataType.DT_FILETIME:
                    _mainOutputProps.Buffer.SetDateTime(_mainOutputProps[index].BufferColumnIndex, DateTime.Parse(columnData, CultureInfo.GetCultureInfo(this.Locale).DateTimeFormat));
                    break;
                case DataType.DT_DBTIME:
                case DataType.DT_DBDATE:
                    _mainOutputProps.Buffer.SetDateTime(_mainOutputProps[index].BufferColumnIndex, DateTime.Parse(columnData, CultureInfo.GetCultureInfo(this.Locale).DateTimeFormat));
                    break;
                case DataType.DT_GUID:
                    _mainOutputProps.Buffer.SetGuid(_mainOutputProps[index].BufferColumnIndex, new Guid(columnData));
                    break;
                case DataType.DT_I1:
                    _mainOutputProps.Buffer.SetSByte(_mainOutputProps[index].BufferColumnIndex, sbyte.Parse(columnData));
                    break;
                case DataType.DT_I2:
                    _mainOutputProps.Buffer.SetInt16(_mainOutputProps[index].BufferColumnIndex, short.Parse(columnData));
                    break;
                case DataType.DT_I4:
                    _mainOutputProps.Buffer.SetInt32(_mainOutputProps[index].BufferColumnIndex, int.Parse(columnData));
                    break;
                case DataType.DT_I8:
                    _mainOutputProps.Buffer.SetInt64(_mainOutputProps[index].BufferColumnIndex, long.Parse(columnData));
                    break;
                case DataType.DT_IMAGE:
                    byte[] imageBytes = _asciiEncoding.GetBytes(columnData);
                    _mainOutputProps.Buffer.AddBlobData(_mainOutputProps[index].BufferColumnIndex, imageBytes);
                    break;
                case DataType.DT_NTEXT:
                    byte[] ntextBytes = _unicodeEncoding.GetBytes(columnData);
                    _mainOutputProps.Buffer.AddBlobData(_mainOutputProps[index].BufferColumnIndex, ntextBytes);
                    break;
                case DataType.DT_NULL:
                    _mainOutputProps.Buffer.SetNull(_mainOutputProps[index].BufferColumnIndex);
                    break;
                case DataType.DT_R4:
                    _mainOutputProps.Buffer.SetSingle(_mainOutputProps[index].BufferColumnIndex, float.Parse(columnData));
                    break;
                case DataType.DT_R8:
                    _mainOutputProps.Buffer.SetDouble(_mainOutputProps[index].BufferColumnIndex, double.Parse(columnData));
                    break;
                case DataType.DT_STR:
                case DataType.DT_WSTR:
                    _mainOutputProps.Buffer.SetString(_mainOutputProps[index].BufferColumnIndex, columnData);
                    break;
                case DataType.DT_TEXT:
                    byte[] textBytes = this.OutputEncoding.GetBytes(columnData);
                    _mainOutputProps.Buffer.AddBlobData(_mainOutputProps[index].BufferColumnIndex, textBytes);
                    break;
                case DataType.DT_UI1:
                    _mainOutputProps.Buffer.SetByte(_mainOutputProps[index].BufferColumnIndex, byte.Parse(columnData));
                    break;
                case DataType.DT_UI2:
                    _mainOutputProps.Buffer.SetUInt16(_mainOutputProps[index].BufferColumnIndex, ushort.Parse(columnData));
                    break;
                case DataType.DT_UI4:
                    _mainOutputProps.Buffer.SetUInt32(_mainOutputProps[index].BufferColumnIndex, uint.Parse(columnData));
                    break;
                case DataType.DT_UI8:
                    _mainOutputProps.Buffer.SetUInt64(_mainOutputProps[index].BufferColumnIndex, ulong.Parse(columnData));
                    break;
                default:
                    throw new FormatException("Unknown target column datatype");
            }
        }

        /// <summary>
        /// Assigns a null value to a specific column on the standard output 
        /// buffer for the current row
        /// </summary>
        /// <param name="index">The zero-based column index</param>
        public void SetNull(int index)
        {
            _mainOutputProps.Buffer.SetNull(index);
        }

        /// <summary>
        /// Adds a row to the log output
        /// </summary>
        /// <param name="errorCode">The error code for the row</param>
        /// <param name="errorMessage">A message describing the error</param>
        /// <param name="lineNumber">The source file line number the error occurred on</param>
        /// <param name="columnName">The column name the error occurred on</param>
        /// <param name="columnData">The column data that caused the error</param>
        public void AddLogRow(int errorCode, string errorMessage, long lineNumber, string columnName, string columnData)
        {
            if (_logOutputProps.Buffer != null)
            {
                PipelineBuffer logBuffer = _logOutputProps.Buffer;
                logBuffer.AddRow();

                logBuffer.SetInt32(_logOutputProps[DelimitedFileSource2005Component.ErrorCodeColumnName].BufferColumnIndex, errorCode);
                logBuffer.SetInt64(_logOutputProps[DelimitedFileSource2005Component.LineNumberColumnName].BufferColumnIndex, lineNumber);
                logBuffer.SetString(_logOutputProps[DelimitedFileSource2005Component.SourceColumnName].BufferColumnIndex, columnName);
                logBuffer.SetDateTime(_logOutputProps[DelimitedFileSource2005Component.OccurredColumnName].BufferColumnIndex, DateTime.Now);

                if (!string.IsNullOrEmpty(columnName))
                {
                    logBuffer.SetString(_logOutputProps[DelimitedFileSource2005Component.TargetColumnDataTypeColumnName].BufferColumnIndex, _mainOutputProps[columnName].DataType.ToString());
                    logBuffer.SetInt32(_logOutputProps[DelimitedFileSource2005Component.TargetColumnLengthColumnName].BufferColumnIndex, _mainOutputProps[columnName].Length);
                }

                if (this.LogAuditingConfig.IncludeValidationMessages)
                    logBuffer.SetString(_logOutputProps[DelimitedFileSource2005Component.DescriptionColumnName].BufferColumnIndex, errorMessage);

                if (this.LogAuditingConfig.IncludeSourceData && !string.IsNullOrEmpty(columnData))
                {
                    int sourceColIndex = _logOutputProps[DelimitedFileSource2005Component.SourceColumnDataColumnName].BufferColumnIndex;
                    SetBufferString(logBuffer, sourceColIndex, this.LogAuditingConfig.SourceDataType, columnData, this.LogAuditingConfig.SourceDataSize);
                }
            }
        }

        /// <summary>
        /// Adds a row to the error output
        /// </summary>
        /// <param name="errorCode">The error code for the row</param>
        /// <param name="errorMessage">A message describing the error</param>
        /// <param name="lineNumber">The source file line number the error occurred on</param>
        /// <param name="rowData">The row data that caused the error</param>
        /// <param name="isInvalid">True if the error was fatal to standard output processing</param>
        /// <param name="columnName">The column name the error occurred on</param>
        /// <param name="columnData">The column data that caused the error</param>
        public void AddErrorRow(int errorCode, string errorMessage, long lineNumber, string rowData, bool isInvalid, string columnName, string columnData)
        {
            if (_errorOutputProps.Buffer != null)
            {
                PipelineBuffer errorBuffer = _errorOutputProps.Buffer;
                errorBuffer.AddRow();

                if (!string.IsNullOrEmpty(columnName))
                {
                    errorBuffer.SetErrorInfo(_errorOutputProps.OutputID, errorCode, _mainOutputProps[columnName].BufferColumnIndex);
                    errorBuffer.SetString(_errorOutputProps[DelimitedFileSource2005Component.SourceColumnName].BufferColumnIndex, _mainOutputProps[columnName].Name);
                }
                else
                {
                    errorBuffer.SetErrorInfo(_errorOutputProps.OutputID, errorCode, 0);
                }
                
                if (this.ErrorAuditingConfig.HasAuditing)
                {
                    errorBuffer.SetInt64(_errorOutputProps[DelimitedFileSource2005Component.LineNumberColumnName].BufferColumnIndex, lineNumber);
                    errorBuffer.SetDateTime(_errorOutputProps[DelimitedFileSource2005Component.OccurredColumnName].BufferColumnIndex, DateTime.Now);
                    errorBuffer.SetBoolean(_errorOutputProps[DelimitedFileSource2005Component.IsInvalidColumnName].BufferColumnIndex, isInvalid);

                    if (!string.IsNullOrEmpty(columnName))
                    {
                        errorBuffer.SetString(_errorOutputProps[DelimitedFileSource2005Component.TargetColumnDataTypeColumnName].BufferColumnIndex, _mainOutputProps[columnName].DataType.ToString());
                        errorBuffer.SetInt32(_errorOutputProps[DelimitedFileSource2005Component.TargetColumnLengthColumnName].BufferColumnIndex, _mainOutputProps[columnName].Length);
                    }
                }

                if (this.ErrorAuditingConfig.IncludeValidationMessages)
                    errorBuffer.SetString(_errorOutputProps[DelimitedFileSource2005Component.DescriptionColumnName].BufferColumnIndex, errorMessage);

                if (this.ErrorAuditingConfig.IncludeSourceData)
                {
                    int sourceColIndex = _errorOutputProps[DelimitedFileSource2005Component.SourceColumnDataColumnName].BufferColumnIndex;
                    SetBufferString(errorBuffer, sourceColIndex, this.ErrorAuditingConfig.SourceDataType, columnData, this.ErrorAuditingConfig.SourceDataSize);

                    int rowColIndex = _errorOutputProps[DelimitedFileSource2005Component.RowDataColumnName].BufferColumnIndex;
                    SetBufferString(errorBuffer, rowColIndex, this.ErrorAuditingConfig.SourceDataType, rowData, this.ErrorAuditingConfig.SourceDataSize);
                }
            }
        }

        /// <summary>
        /// Sets the data for a string column on the specified buffer
        /// </summary>
        /// <param name="buffer">The PipelineBuffer containing the column to set the data on</param>
        /// <param name="colindex">The column index identifying the column within the PipelineBuffer</param>
        /// <param name="datatype">The string based datatype of the column</param>
        /// <param name="data">The string data to save to the column</param>
        /// <param name="maxSize">The maximum size of data to be saved to the column</param>
        private void SetBufferString(PipelineBuffer buffer, int colindex, DataType datatype, string data, int maxSize)
        {
            if (string.IsNullOrEmpty(data))
                return;

            switch (datatype)
            {
                case DataType.DT_NTEXT:
                    byte[] ntextBytes = _unicodeEncoding.GetBytes(data);
                    buffer.AddBlobData(colindex, ntextBytes);
                    break;
                case DataType.DT_STR:
                case DataType.DT_WSTR:
                    buffer.SetString(colindex, CommonUtils.LimitStringLength(data, maxSize));
                    break;
                case DataType.DT_TEXT:
                    byte[] textBytes = this.OutputEncoding.GetBytes(data);
                    buffer.AddBlobData(colindex, textBytes);
                    break;
            }
        }
    }
}
